package org.xx.armory.swing.components;

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import java.awt.Image;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

import static java.util.Arrays.asList;
import static org.xx.armory.commons.SysUtils.stringEquals;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;
import static org.xx.armory.commons.Validators.validIndex;
import static org.xx.armory.swing.UIUtils.toBigDecimalObject;
import static org.xx.armory.swing.UIUtils.toBooleanObject;
import static org.xx.armory.swing.UIUtils.toDateObject;
import static org.xx.armory.swing.UIUtils.toDoubleObject;
import static org.xx.armory.swing.UIUtils.toLongObject;

/**
 * 为每一列定义类型的表格模型。
 * <p>该表格数据模型一旦定义后，结构不可更改。</p>
 */
public class TypedTableModel
        extends AbstractTableModel
        implements TableModel, Serializable {
    public static final String ROW_ID_NAME = "_row";

    private static final long serialVersionUID = 1L;

    private List<TypedTableColumn> columns;
    private ArrayList<Map<String, Object>> rows;
    private Map<String, Map<String, String>> lookUpTables;

    /**
     * 构造表格数据模型实例。
     *
     * @param columns
     *         表格数据模型的所有列。
     * @throws IllegalArgumentException
     *         如果参数{@code columns}是{@code null}。
     */
    public TypedTableModel(
            Collection<? extends TypedTableColumn> columns
    ) {
        notNull(columns, "columns");

        this.columns = new ArrayList<>(columns);
        this.rows = new ArrayList<>();
    }

    /**
     * 构造表格数据模型实例。
     *
     * @param columns
     *         表格数据模型的所有列。
     * @throws IllegalArgumentException
     *         如果参数{@code columns}是{@code null}。
     */
    public TypedTableModel(
            TypedTableColumn... columns
    ) {
        notNull(columns, "columns");

        this.columns = new ArrayList<>(asList(columns));
        this.rows = new ArrayList<>();
        this.lookUpTables = new HashMap<>();
    }

    /**
     * 设置所有的行。
     * <p>首先清除原来的所有行，然后加入新的行。</p>
     *
     * @param rows
     *         所有的行。
     */
    public void setAllRows(
            Collection<? extends Map<String, Object>> rows
    ) {
        this.rows.clear();

        if (rows != null) {
            this.rows.ensureCapacity(rows.size());
            int rowIndex = 0;
            for (final Map<String, Object> row : rows) {
                final Map<String, Object> row_ = new HashMap<>(row);
                row_.put(ROW_ID_NAME, ++rowIndex);
                this.rows.add(row_);
            }
        }

        super.fireTableDataChanged();
    }

    /**
     * 设置指定的行。
     *
     * @param rowIndex
     *         行号。
     * @param row
     *         指定行号的新行。
     * @throws ArrayIndexOutOfBoundsException
     *         如果参数{@code rowIndex}小于0或者大于最大行号。
     * @throws IllegalArgumentException
     *         如果参数{@code row}是{@code null}。
     */
    public void setRow(
            int rowIndex,
            Map<String, Object> row
    ) {
        validIndex(rowIndex, "rowIndex", this.rows.size() - 1);
        notNull(row, "row");

        final Map<String, Object> row_ = new HashMap<>(row);
        row_.put(ROW_ID_NAME, this.rows.get(rowIndex).get(ROW_ID_NAME));
        this.rows.set(rowIndex, row_);

        super.fireTableRowsUpdated(rowIndex, rowIndex);
    }

    /**
     * 插入新的行。
     * <p>将新行插入指定的行号，该行号原来的行和后续的行依次向后移动一行。</p>
     *
     * @param rowIndex
     *         行号。
     * @param row
     *         指定行号的新行。
     * @throws ArrayIndexOutOfBoundsException
     *         如果参数{@code rowIndex}小于0或者大于最大行号。
     * @throws IllegalArgumentException
     *         如果参数{@code row}是{@code null}。
     */
    public void insertRow(
            int rowIndex,
            Map<String, Object> row
    ) {
        notNull(row, "row");

        row.put(ROW_ID_NAME, this.rows.size() + 1);

        if (!this.rows.isEmpty()) {
            // 如果当前已存在数据，那么执行插入。
            validIndex(rowIndex, "rowIndex", this.rows.size() - 1);

            this.rows.add(rowIndex, row);
        } else {
            // 否则执行添加。
            validIndex(rowIndex, "rowIndex", 0);

            this.rows.add(row);
        }

        super.fireTableRowsInserted(rowIndex, rowIndex);
    }

    /**
     * 添加新的行。
     * <p>将新行追加到最后。</p>
     *
     * @param row
     *         待添加的新行。
     * @throws IllegalArgumentException
     *         如果参数{@code row}是{@code null}。
     */
    public void addRow(
            Map<String, Object> row
    ) {
        notNull(row, "row");

        final int rowIndex = this.rows.size();
        row.put(ROW_ID_NAME, rowIndex + 1);
        this.rows.add(row);

        super.fireTableRowsInserted(rowIndex, rowIndex);
    }

    /**
     * 删除已存在的行。
     *
     * @param rowIndex
     *         行号。
     * @throws ArrayIndexOutOfBoundsException
     *         如果参数{@code rowIndex}小于0或者大于最大行号。
     */
    public void removeRow(
            int rowIndex
    ) {
        validIndex(rowIndex, "rowIndex", this.rows.size() - 1);

        this.rows.remove(rowIndex);

        super.fireTableRowsDeleted(rowIndex, rowIndex);
    }

    /**
     * 删除第一个满足条件的行。
     * <p>如果没有任何行满足条件则不删除任何行。</p>
     *
     * @param predicate
     *         删除条件。
     * @throws IllegalArgumentException
     *         如果参数{@code predicate}是{@code null}。
     */
    public void removeFirstRow(
            Predicate<? super Map<String, Object>> predicate
    ) {
        notNull(predicate, "predicate");

        int rowIndex = -1;
        for (int i = 0; i < this.rows.size(); ++i) {
            if (predicate.test(this.rows.get(i))) {
                rowIndex = i;
                break;
            }
        }

        if (rowIndex < 0) {
            return;
        }

        this.rows.remove(rowIndex);

        super.fireTableRowsDeleted(rowIndex, rowIndex);
    }

    /**
     * 为指定的列设置查找表。
     * <p>如果参数{@code lookUpTable}是{@code null}，那么删除该列原有的查找表（如果存在）。</p>
     *
     * @param name
     *         列名。
     * @param lookUpTable
     *         查找表。
     * @throws IllegalArgumentException
     *         如果参数{@code name}是{@code null}或者只包含空白字符。
     */
    public void setLookUpTable(
            String name,
            Map<String, String> lookUpTable
    ) {
        name = notBlank(name, "name").trim();
        if (lookUpTable != null) {
            this.lookUpTables.put(name, lookUpTable);
        } else {
            this.lookUpTables.remove(name);
        }
    }

    /**
     * 获取表格数据模型所有的列。
     * <p>返回结果是只读的，不能通过返回结果修改数据模型的结构。</p>
     *
     * @return 表格数据模型所有的列。
     */
    public List<TypedTableColumn> getAllColumns() {
        return Collections.unmodifiableList(this.columns);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getRowCount() {
        return this.rows.size();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getColumnCount() {
        return this.columns.size();
    }

    @Override
    public Object getValueAt(
            int rowIndex,
            int columnIndex
    ) {
        if (columnIndex >= this.columns.size()) {
            return null;
        }
        final TypedTableColumn column = this.columns.get(columnIndex);
        if (column == null) {
            return null;
        }

        final Map<String, Object> row = this.rows.get(rowIndex);
        if (row == null) {
            return null;
        }

        final Object obj = row.get(column.getName());
        if (obj == null) {
            return null;
        }

        switch (column.getType()) {
            case TEXT:
                if (this.lookUpTables.containsKey(column.getName())) {
                    // 如果该列有关联的查找表，那么执行查找，查找结果是文本。
                    return this.lookUpTables.get(column.getName()).get(obj.toString());
                } else {
                    return obj.toString();
                }
            case CHECK_BOX:
                return parseBoolean(obj);
            case YES_OR_NO:
                return parseBoolean(obj);
            case IMAGE:
                if (obj instanceof byte[]) {
                    return new ImageIcon((byte[]) obj);
                } else if (obj instanceof Image) {
                    return new ImageIcon((Image) obj);
                } else {
                    return null;
                }
            case NUMBER:
                if (obj instanceof Number) {
                    return obj;
                } else if (obj instanceof String) {
                    try {
                        return Long.parseLong((String) obj);
                    } catch (NumberFormatException ex) {
                        return null;
                    }
                }
            case NUMBER_TEXT:
                if (this.lookUpTables.containsKey(column.getName())) {
                    // 如果该列有关联的查找表，那么执行查找，查找结果是文本。
                    return this.lookUpTables.get(column.getName()).get(obj.toString());
                } else {
                    // 否则是整数。
                    if (obj instanceof Number) {
                        return obj;
                    } else if (obj instanceof String) {
                        try {
                            return Long.parseLong((String) obj);
                        } catch (NumberFormatException ex) {
                            return null;
                        }
                    }
                }
            case FLOAT:
                if (obj instanceof BigDecimal) {
                    return obj;
                } else if (obj instanceof Number) {
                    return new BigDecimal(obj.toString()).setScale(2, RoundingMode.HALF_EVEN);
                } else if (obj instanceof String) {
                    try {
                        return new BigDecimal((String) obj).setScale(2, RoundingMode.HALF_EVEN);
                    } catch (NumberFormatException ex) {
                        return null;
                    }
                }
            case CURRENCY:
                if (obj instanceof BigDecimal) {
                    return obj;
                } else if (obj instanceof Number) {
                    return new BigDecimal(obj.toString()).setScale(2, RoundingMode.HALF_EVEN);
                } else if (obj instanceof String) {
                    try {
                        return new BigDecimal((String) obj).setScale(2, RoundingMode.HALF_EVEN);
                    } catch (NumberFormatException ex) {
                        return null;
                    }
                }
            case PERCENT:
                if (obj instanceof BigDecimal) {
                    return obj;
                } else if (obj instanceof Number) {
                    return new BigDecimal(obj.toString()).setScale(4, RoundingMode.HALF_EVEN);
                } else if (obj instanceof String) {
                    try {
                        return new BigDecimal((String) obj).setScale(4, RoundingMode.HALF_EVEN);
                    } catch (NumberFormatException ex) {
                        return null;
                    }
                }
            case DATE:
            case DATE_TIME:
                if (obj instanceof Number) {
                    return new Date(((Number) obj).longValue());
                } else if (obj instanceof Date) {
                    return obj;
                } else {
                    return null;
                }
            default:
                return null;
        }
    }

    public String getStringByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        return value != null ? value.toString() : "";
    }

    public Long getNumberByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        if (value instanceof Number) {
            return ((Number) value).longValue();
        } else if (value instanceof String) {
            return toLongObject((String) value);
        } else {
            return null;
        }
    }

    public Double getFloatByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        if (value instanceof Number) {
            return ((Number) value).doubleValue();
        } else if (value instanceof String) {
            return toDoubleObject((String) value);
        } else {
            return null;
        }
    }

    public Boolean getBooleanByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        if (value instanceof Boolean) {
            return (Boolean) value;
        } else if (value instanceof Number) {
            return ((Number) value).intValue() != 0;
        } else if (value instanceof String) {
            return toBooleanObject((String) value);
        } else {
            return null;
        }
    }

    public BigDecimal getBigDecimalByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        if (value instanceof BigDecimal) {
            return (BigDecimal) value;
        } else if (value instanceof Number) {
            return new BigDecimal(((Number) value).doubleValue());
        } else if (value instanceof String) {
            return toBigDecimalObject((String) value);
        } else {
            return null;
        }
    }

    public Date getDateByName(
            int rowIndex,
            String name
    ) {
        final Object value = getValueByName(rowIndex, name);
        if (value instanceof Date) {
            return (Date) value;
        } else if (value instanceof Number) {
            return new Date(((Number) value).longValue());
        } else if (value instanceof String) {
            return toDateObject((String) value);
        } else {
            return null;
        }
    }

    /**
     * 根据行号和列名获取值。
     * <p><strong>此方法返回数据模型中的原始数据，不会处理必要的类型转换，也不会进行格式化以及处理查找表。</strong></p>
     *
     * @param rowIndex
     *         行号，从0开始。
     * @param name
     *         列名。
     * @return 表格模型中的值，如果找不到指定的列或者行，则返回{@code null}。
     */
    public Object getValueByName(
            int rowIndex,
            String name
    ) {
        if (rowIndex >= this.rows.size()) {
            return null;
        }

        final TypedTableColumn column = this.columns
                .stream()
                .filter(c -> stringEquals(name, c.getName()))
                .findAny()
                .orElse(null);

        if (column == null) {
            return null;
        }

        return this.rows.get(rowIndex).get(column.getName());
    }

    private Boolean parseBoolean(
            Object value
    ) {
        notNull(value, "value");

        if (value instanceof Number) {
            return ((Number) value).intValue() != 0;
        } else if (value instanceof String) {
            return toBooleanObject((String) value);
        } else if (value instanceof Boolean) {
            return (Boolean) value;
        } else {
            return null;
        }
    }

    @Override
    public String getColumnName(
            int index
    ) {
        return this.columns.get(index).getTitle();
    }

    @Override
    public Class<?> getColumnClass(
            int columnIndex
    ) {
        if (columnIndex >= this.columns.size()) {
            return Object.class;
        }
        final TypedTableColumn column = this.columns.get(columnIndex);
        if (column == null) {
            return Object.class;
        }

        switch (column.getType()) {
            case TEXT:
                return String.class;
            case CHECK_BOX:
                return Boolean.class;
            case YES_OR_NO:
            case IMAGE:
                return Icon.class;
            case NUMBER:
                return Long.class;
            case NUMBER_TEXT:
                if (this.lookUpTables.containsKey(column.getName())) {
                    // 如果该列有关联的查找表，那么列值是文本。
                    return String.class;
                } else {
                    // 否则是整数。
                    return Long.class;
                }
            case FLOAT:
                return BigDecimal.class;
            case CURRENCY:
                return BigDecimal.class;
            case PERCENT:
                return BigDecimal.class;
            case DATE:
            case DATE_TIME:
                return Date.class;
            default:
                return String.class;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCellEditable(
            int rowIndex,
            int columnIndex
    ) {
        if (columnIndex >= this.columns.size()) {
            return false;
        }
        final TypedTableColumn column = this.columns.get(columnIndex);
        if (column == null) {
            return false;
        }

        // 只有CheckBox列是可以编辑的。
        return column.getType() == TypedTableColumnType.CHECK_BOX;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setValueAt(
            Object value,
            int rowIndex,
            int columnIndex
    ) {
        if (columnIndex >= this.columns.size()) {
            return;
        }
        final TypedTableColumn column = this.columns.get(columnIndex);
        if (column == null) {
            return;
        }
        if (rowIndex >= this.rows.size()) {
            return;
        }
        final Map<String, Object> row = this.rows.get(rowIndex);
        if (row == null) {
            return;
        }

        final String columnName = column.getName();
        row.put(columnName, value);

        super.fireTableRowsUpdated(rowIndex, rowIndex);
    }
}
